/**
 * jaroverlay-maven-plugin : add remove replace content and repackage nested archives
 * 
 * Copyright (C) 2008  Paul Merlin
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.n0pe.jaroverlay;


import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;

import org.n0pe.jaroverlay.config.Overlay;
import org.n0pe.jaroverlay.config.Put;
import org.n0pe.jaroverlay.io.ZipUtil;
import org.n0pe.jaroverlay.model.Archive;


/**
 * This first version will not allow any globbing.
 * 
 * @goal jaroverlay
 * @description JarOverlay
 * @author Paul Merlin <eskatos@n0pe.org>
 */
public class JarOverlayMojo
        extends AbstractMojo {


    // ------------------------------------------------------------------------------------------------------- CONSTANTS
    private static final String ARCHIVE_SEPARATOR = "|";


    private static final String ARCHIVE_SEPARATOR_ESCAPED = "\\" + ARCHIVE_SEPARATOR;


    // -------------------------------------------------------------------------------------------- PLUGIN CONFIGURATION
    /**
     * @parameter
     * @required
     */
    protected Overlay overlay;


    /**
     * @parameter expression="${project.build.directory}/overlayed.zip"
     * @required
     */
    private File overlayedDestinationFile;

    // -------------------------------------------------------------------------------------- MAVEN READ ONLY INJECTIONS
    /**
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    protected MavenProject project;


    /**
     * @component
     */
    protected MavenProjectHelper projectHelper;


    /** 
     * @component
     * @required
     * @readonly
     */
    private ArtifactFactory artifactFactory;


    /** 
     * @component
     * @required
     * @readonly
     */
    private ArtifactResolver artifactResolver;


    /**
     * @parameter expression="${localRepository}"
     * @required
     * @readonly
     */
    private ArtifactRepository localRepository;


    /** 
     * @parameter expression="${project.remoteArtifactRepositories}" 
     * @required
     * @readonly
     */
    private List remoteRepositories;


    // -------------------------------------------------------------------------------------------------- RUNTIME FIELDS
    private File sourceArtifactFile;


    private File unpackedSourceDirectory;


    private File unpackedNestedDirectory;


    private File repackedNestedDirectory;


    private Archive rootArchive;


    private Artifact sourceArtifact;


    private String sourceArtifactExtension;


    public void execute()
            throws MojoExecutionException, MojoFailureException {
        try {
            setupMojo();
            getLog().info("Unpacking all archives and applying overlay");
            unpackAllArchivesAndApplyOverlay();
            repackAllArchives();
            getLog().info("Attaching produced archive with classifier: " + overlay.getClassifier());
            projectHelper.attachArtifact(project,
                                         sourceArtifact.getType(),
                                         overlay.getClassifier(),
                                         overlayedDestinationFile);
        } catch (ArtifactResolutionException ex) {
            getLog().error(ex.getMessage());
            throw new MojoExecutionException(ex.getMessage(), ex);
        } catch (ArtifactNotFoundException ex) {
            getLog().error(ex.getMessage());
            throw new MojoExecutionException(ex.getMessage(), ex);
        } catch (IOException ex) {
            getLog().error(ex.getMessage());
            throw new MojoExecutionException(ex.getMessage(), ex);
        }
    }


    private void setupMojo()
            throws IOException, ArtifactResolutionException, ArtifactNotFoundException, MojoExecutionException {
        // Basic configuration check
        if (overlay == null) {
            throw new MojoExecutionException("No overlay defined");
        }
        if (StringUtils.isEmpty(overlay.getClassifier())) {
            throw new MojoExecutionException("No classifier defined");
        }
        if (overlay.getSource() == null) {
            throw new MojoExecutionException("No source defined");
        }
        if (!overlay.getSource().isValid()) {
            throw new MojoExecutionException("Defined source is not valid");
        }
        unpackedSourceDirectory = new File(project.getBuild().getDirectory() +
                                           File.separator + "jaroverlay" +
                                           File.separator + overlay.getClassifier() +
                                           File.separator + "source");
        unpackedNestedDirectory = new File(project.getBuild().getDirectory() +
                                           File.separator + "jaroverlay" +
                                           File.separator + overlay.getClassifier() +
                                           File.separator + "unpackedNested");
        repackedNestedDirectory = new File(project.getBuild().getDirectory() +
                                           File.separator + "jaroverlay" +
                                           File.separator + overlay.getClassifier() +
                                           File.separator + "repackedNested");
        // Preparing directories
        if (unpackedSourceDirectory.exists()) {
            FileUtils.deleteDirectory(unpackedSourceDirectory);
        }
        if (unpackedNestedDirectory.exists()) {
            FileUtils.deleteDirectory(unpackedNestedDirectory);
        }
        if (repackedNestedDirectory.exists()) {
            FileUtils.deleteDirectory(repackedNestedDirectory);
        }
        if (!unpackedNestedDirectory.mkdirs() || !unpackedSourceDirectory.mkdirs() || !repackedNestedDirectory.mkdirs()) {
            throw new IOException("Could not create temporary directories");
        }
        if (overlayedDestinationFile.exists()) {
            if (!overlayedDestinationFile.delete()) {
                throw new IOException("Could not delete existing overlayed destination file");
            }
        }
        // Downloading source artifact
        getLog().debug("Downloading source artifact: " + overlay.getSource().toString());
        sourceArtifact = artifactFactory.createArtifactWithClassifier(overlay.getSource().getGroupId(),
                                                                      overlay.getSource().getArtifactId(),
                                                                      overlay.getSource().getVersion(),
                                                                      overlay.getSource().getType(),
                                                                      overlay.getSource().getClassifier());
        artifactResolver.resolve(sourceArtifact, remoteRepositories, localRepository);
        sourceArtifactFile = sourceArtifact.getFile();
        sourceArtifactExtension = sourceArtifact.getArtifactHandler().getExtension();
        getLog().debug("Source artifact downloaded successfully");
    }


    private void unpackAllArchivesAndApplyOverlay()
            throws IOException {
        // Source artifact file
        getLog().debug("Unpacking source artifact to " + unpackedSourceDirectory.getAbsolutePath());
        ZipUtil.unzip(sourceArtifactFile, unpackedSourceDirectory);
        rootArchive = Archive.create(null, sourceArtifactFile, unpackedSourceDirectory);

        if (overlay.getRemoves() != null) {
            // Process all archives paths for removes
            getLog().info("Processing " + overlay.getRemoves().length + " remove directive(s)");
            for (int i = 0; i < overlay.getRemoves().length; i++) {
                String archivePath = overlay.getRemoves()[i];
                Archive curParentArchive = rootArchive;
                getLog().debug("REMOVE: " + archivePath);
                if (archivePath.contains(ARCHIVE_SEPARATOR)) {
                    String[] splittedArchivePath = archivePath.split(ARCHIVE_SEPARATOR_ESCAPED);
                    for (int j = 0; j < splittedArchivePath.length - 1; j++) {
                        String archivePathItem = splittedArchivePath[j];
                        File curPackedFile = new File(curParentArchive.getUnpackedDirectory() +
                                                      File.separator + archivePathItem);
                        File curUnpackedDirectory;
                        if (archivePathItem.contains("/")) {
                            curUnpackedDirectory = new File(unpackedNestedDirectory,
                                                            archivePathItem.substring(archivePathItem.lastIndexOf("/")));
                        } else {
                            curUnpackedDirectory = new File(unpackedNestedDirectory, archivePathItem);
                        }
                        if (!curUnpackedDirectory.exists()) {
                            getLog().debug("REMOVE: Unpacking " + curPackedFile.getAbsolutePath() + " to " +
                                           curUnpackedDirectory.getAbsolutePath());
                            ZipUtil.unzip(curPackedFile, curUnpackedDirectory);
                        }
                        curParentArchive = Archive.create(curParentArchive,
                                                          curPackedFile,
                                                          curUnpackedDirectory,
                                                          new Integer(splittedArchivePath.length));
                    }
                    // Remove
                    File toDel = new File(curParentArchive.getUnpackedDirectory().getAbsolutePath() + File.separator +
                                          splittedArchivePath[splittedArchivePath.length - 1]);
                    if (toDel.exists()) {
                        if (toDel.isDirectory()) {
                            getLog().debug("REMOVE: deleting directory: " + toDel.getAbsolutePath());
                            FileUtils.deleteDirectory(toDel);
                        } else {
                            getLog().debug("REMOVE: deleting file: " + toDel.getAbsolutePath());
                            toDel.delete();
                        }
                    } else {
                        getLog().warn("REMOVE: file to delete does not exists: " + toDel.getAbsolutePath());
                    }
                } else {
                    // Remove
                    File toDel = new File(curParentArchive.getUnpackedDirectory().getAbsolutePath() +
                                          File.separator + archivePath);
                    if (toDel.exists()) {
                        if (toDel.isDirectory()) {
                            getLog().debug("REMOVE: deleting directory: " + toDel.getAbsolutePath());
                            FileUtils.deleteDirectory(toDel);
                        } else {
                            getLog().debug("REMOVE: deleting file: " + toDel.getAbsolutePath());
                            toDel.delete();
                        }
                    } else {
                        getLog().warn("REMOVE: file to delete does not exists: " + toDel.getAbsolutePath());
                    }
                }
            }
        } else {
            getLog().debug("No remove directive for this overlay");
        }

        if (overlay.getPuts() != null) {
            // Process all archives paths for puts
            getLog().info("Processing " + overlay.getPuts().length + " put directive(s)");
            for (int i = 0; i < overlay.getPuts().length; i++) {
                Put put = overlay.getPuts()[i];
                String archivePath = put.getTo();
                Archive curParentArchive = rootArchive;
                getLog().debug("PUT: " + archivePath);
                if (archivePath.contains(ARCHIVE_SEPARATOR)) {
                    String[] splittedArchivePath = archivePath.split(ARCHIVE_SEPARATOR_ESCAPED);
                    for (int j = 0; j < splittedArchivePath.length - 1; j++) {
                        String archivePathItem = splittedArchivePath[j];
                        File curPackedFile = new File(curParentArchive.getUnpackedDirectory() +
                                                      File.separator + archivePathItem);
                        File curUnpackedDirectory;
                        if (archivePathItem.contains("/")) {
                            curUnpackedDirectory = new File(unpackedNestedDirectory,
                                                            archivePathItem.substring(archivePathItem.lastIndexOf("/")));
                        } else {
                            curUnpackedDirectory = new File(unpackedNestedDirectory, archivePathItem);
                        }
                        if (!curUnpackedDirectory.exists()) {
                            getLog().debug("PUT: Unpacking " + curPackedFile.getAbsolutePath() + " in " +
                                           curUnpackedDirectory.getAbsolutePath());
                            ZipUtil.unzip(curPackedFile, curUnpackedDirectory);
                        }
                        curParentArchive = Archive.create(curParentArchive,
                                                          curPackedFile,
                                                          curUnpackedDirectory,
                                                          new Integer(splittedArchivePath.length));
                    }
                    // Put
                    if (put.getFrom().exists()) {
                        File dest = new File(curParentArchive.getUnpackedDirectory().getAbsolutePath() +
                                             File.separator +
                                             splittedArchivePath[splittedArchivePath.length - 1]);
                        if (dest.exists()) {
                            getLog().debug("PUT: Will overwrite the following file: " + dest.getAbsolutePath());
                        }
                        if (put.getFrom().isDirectory()) {
                            getLog().debug("PUT: Putting directory " + put.getFrom().getAbsolutePath() +
                                          " to " + dest.getAbsolutePath());
                            FileUtils.copyDirectory(put.getFrom(), dest);
                        } else {
                            getLog().debug("PUT: Putting file " + put.getFrom().getAbsolutePath() +
                                          " to " + dest.getAbsolutePath());
                            FileUtils.copyFile(put.getFrom(), dest);
                        }
                    } else {
                        getLog().warn("PUT: from file does not exists: " + put.getFrom().getAbsolutePath());
                    }
                } else {
                    // Put
                    if (put.getFrom().exists()) {
                        File dest = new File(curParentArchive.getUnpackedDirectory().getAbsolutePath() +
                                             File.separator + archivePath);
                        if (dest.exists()) {
                            getLog().debug("PUT: Will overwrite the following file: " + dest.getAbsolutePath());
                        }
                        if (put.getFrom().isDirectory()) {
                            getLog().debug("PUT: Putting directory " + put.getFrom().getAbsolutePath() +
                                          " to " + dest.getAbsolutePath());
                            FileUtils.copyDirectory(put.getFrom(), dest);
                        } else {
                            getLog().debug("PUT: Putting file " + put.getFrom().getAbsolutePath() +
                                          " to " + dest.getAbsolutePath());
                            FileUtils.copyFile(put.getFrom(), dest);
                        }
                    } else {
                        getLog().warn("PUT: from file does not exists: " + put.getFrom().getAbsolutePath());
                    }
                }
            }
        } else {
            getLog().debug("No put directive for this overlay");
        }
    }


    private void repackAllArchives()
            throws IOException {
        getLog().info("Repacking all archives ...");
        // Gather leafs
        Stack leafs = new Stack();
        Stack tempStack = new Stack();
        tempStack.add(rootArchive);
        while (!tempStack.empty()) {
            Archive currentArchive = (Archive) tempStack.pop();
            if (currentArchive.getChildren().isEmpty()) {
                leafs.add(currentArchive);
            } else {
                for (Iterator it = currentArchive.getChildren().iterator(); it.hasNext();) {
                    tempStack.push((Archive) it.next());
                }
            }
        }
        // Climb the archive graph
        tempStack = new Stack();
        while (!leafs.empty()) {
            Archive currentLeaf = (Archive) leafs.pop();
            tempStack.add(currentLeaf);
            if (currentLeaf.getParent() != null) {
                if (tempStack.contains(currentLeaf.getParent())) {
                    // WARN: Apparement ca ne fonctionne pas ca
                    tempStack.remove(currentLeaf.getParent());
                }
                tempStack.push(currentLeaf.getParent());
                leafs.push(currentLeaf.getParent());
            }
        }
        // Reverse the stack
        Stack finalStack = new Stack();
        while (!tempStack.empty()) {
            finalStack.push((Archive) tempStack.pop());
        }
        getLog().debug(finalStack.toString());
        // Process the final stack
        while (!finalStack.empty()) {
            Archive currentArchive = (Archive) finalStack.pop();
            File repacked = new File(repackedNestedDirectory, currentArchive.getPackedSourceFile().getName());
            // if (!repacked.exists()) {
            getLog().debug("REPACK: Packing " + currentArchive.getUnpackedDirectory().getAbsolutePath() +
                          " to " + repacked.getAbsolutePath());
            ZipUtil.recursiveZip(currentArchive.getUnpackedDirectory(), repacked);
            currentArchive.setRepackedFile(repacked);
            FileUtils.copyFile(repacked, currentArchive.getPackedSourceFile());
        // }
        }
        overlayedDestinationFile = rootArchive.getRepackedFile();

        overlayedDestinationFile = new File(project.getBuild().getDirectory(),
                                            project.getArtifactId() + "-" +
                                            project.getVersion() + "-" +
                                            overlay.getClassifier() + "." + sourceArtifactExtension);
        FileUtils.copyFile(rootArchive.getRepackedFile(), overlayedDestinationFile);

        getLog().info("Overlayed artifact is here: " + overlayedDestinationFile.getAbsolutePath());
    }


}